feat: named RetryCurve API for switching retry regimes at runtime - #68
feat: named RetryCurve API for switching retry regimes at runtime#68tanderson-ld wants to merge 3 commits into
Conversation
Introduces a RetryCurve opaque handle. Callers construct curves via NewRetryCurve(options...), designate them at subscribe time via StreamOptionDefaultRetryCurve / StreamOptionRegisterRetryCurve, and switch between them at runtime via Stream.ActivateCurve. Enables SDKs to run a multi-regime retry policy (e.g., a normal regime + an extended regime for auth failures) while keeping the library's single-regime timing path intact for legacy callers. Overlay resolution walks (active-curve spec -> effective-default spec -> hard-coded fallbacks), evaluated lazily at delay-computation time. Per-curve formula counter n is retained across activations. Healthy-operation reset zeros all curves' formula counters and reverts to the effective default; it does not clear base-delay overrides (matches SSE spec's "reconnection time is set until updated"). SSE `retry:` field is honored per HTML5 semantics: the stream read loop updates every registered curve's base-delay override. Values above 1 hour are clamped per RETRY spec section 1.11.4 (new MaxServerDirectedRetryDelay constant). Clamping happens in milliseconds before the multiplication by time.Millisecond so extreme wire values cannot overflow the Duration. Internal changes: - Widened backoffStrategy.applyBackoff and jitterStrategy.applyJitter to accept per-call maxDelay / ratio so a single strategy instance can serve multiple curves. Math bodies unchanged from the pre-existing library. - Renamed internal SetBaseDelay to ApplyRetryTime; it now iterates all registered curves. Legacy stream options (StreamOptionInitialRetry / UseBackoff / UseJitter / RetryResetInterval) continue to work unchanged; when no explicit RetryCurve is provided they synthesize the effective default. Refs SDK-2788.
| pub := ev.(*publication) | ||
| if pub.Retry() > 0 { | ||
| stream.retryDelay.SetBaseDelay(time.Duration(pub.Retry()) * time.Millisecond) | ||
| stream.retryDelay.ApplyRetryTime(clampServerDirectedRetry(pub.Retry())) |
There was a problem hiding this comment.
For reviewers: renamed, existing name was misleading, even on main this did more than set base delay.
|
|
||
| var delayedEvent eventOrComment | ||
| jitterStrategy := newDefaultJitter(0.5, 0) | ||
| jitterStrategy := newDefaultJitter(0) |
There was a problem hiding this comment.
For reviewers: Jitter is now passed as a param to the strategy at jitter application time.
| type backoffStrategy interface { | ||
| applyBackoff(baseDelay time.Duration, retryCount int) time.Duration | ||
| applyBackoff(baseDelay time.Duration, retryCount int, maxDelay time.Duration) time.Duration | ||
| } |
There was a problem hiding this comment.
For reviewers: backoffStrategy and jitterStrategy are internal only interfaces. The max and jitter are now properties of the retry curve and not fixed in the strategy.
| } | ||
|
|
||
| type defaultJitterStrategy struct { | ||
| ratio float64 |
There was a problem hiding this comment.
For reviewers: jitter ratio moved to the retry curve to support cases of different jitters in different sitatuions.
| // streamOptions. | ||
| func newRetryDelayStrategyFromOptions(opts *streamOptions, randSeed int64) *retryDelayStrategy { | ||
| // Resolve the effective default curve. | ||
| effectiveDefault := opts.defaultRetryCurve |
There was a problem hiding this comment.
For reviewers: if a default curve was not provided, we will make a default curve from the old stream options in order to not be a breaking change.
| func (r *retryDelayStrategy) SetBaseDelay(baseDelay time.Duration) { | ||
| // Does NOT reset the newly-activated curve's retryCount — each curve's counter | ||
| // retains its progression across activations. Does NOT touch any curve's | ||
| // baseDelayOverride. |
There was a problem hiding this comment.
For reviewers: the baseDelayOverride is set via the server directed retry: event.
- Rename `max` parameter on RetryCurveMaxDelay to `maxDelay` (revive redefines-builtin-id: `max` shadows the Go 1.21 built-in). - Add `//nolint:unused // used only in tests` to activeCurve, matching the existing convention on hasJitter. - Wrap the applyBackoff signature and the three firstNonNil calls in resolveCurveProperties across multiple lines (lll: 120-char limit). No logic changes. `make lint` and `go test ./...` both clean locally.
Summary
Adds a
RetryCurveAPI so a caller can register multiple retry-timing curves on a single stream and switch between them at runtime viaStream.ActivateCurve. Enables consumers to run a multi-regime retry policy (e.g., a normal regime + an extended regime for auth failures) while keeping the library's single-regime timing path intact for existing callers.Marked draft for API/design socialization before dependent SDK work builds on it.
API additions
NewRetryCurve(options ...RetryCurveOption) *RetryCurve— construct an opaque handle.RetryCurveBaseDelay(d),RetryCurveMaxDelay(d),RetryCurveJitter(r)— curve options.StreamOptionDefaultRetryCurve(curve)— designate as the stream's effective default.StreamOptionRegisterRetryCurve(curve)— register as an additional switchable curve.Stream.ActivateCurve(curve *RetryCurve)— switch the currently-active curve at runtime.DefaultCurve— package-level sentinel meaning "revert to the effective default."MaxServerDirectedRetryDelay = time.Hour— clamp ceiling for the SSEretry:field.Legacy stream options (
StreamOptionInitialRetry,StreamOptionUseBackoff,StreamOptionUseJitter,StreamOptionRetryResetInterval) continue to work unchanged; when no explicitRetryCurveis provided they synthesize the effective default.Semantics
active-curve.spec→effective-default.spec→ hard-coded fallback. Curve specs are immutable.n. Each registered curve tracks its own backoff-formula counter. Progression is retained across activations, so rapid oscillation between regimes preserves each regime's state.elapsed >= resetInterval, zeros every curve'snand revertsactiveto the effective default. Does NOT clear server-directed base-delay overrides (matches HTML5 SSE spec's "reconnection time is set until updated").retry:field. Updates every registered curve's base-delay override (stream-wide per HTML5). Never touches any curve's declaredmaxDelayceiling.retry:values aboveMaxServerDirectedRetryDelayare clamped, in milliseconds before thetime.Millisecondmultiplication, so extreme int64 wire values cannot overflow theDuration.Internal notes for reviewers
backoffStrategy.applyBackoffandjitterStrategy.applyJitterinterfaces were widened to accept per-callmaxDelay/ratio. Math bodies are unchanged from the pre-existing library; only the parameter source moved from receiver fields to method args, so one strategy instance can serve multiple curves.SetBaseDelayrenamed toApplyRetryTime; it now iterates all registered curves.Test plan
go test ./...).make contract-tests— "All tests passed").Refs SDK-2788.